#!/usr/bin/env python

#############################################################################
# client.py: Contains the Client class and is the entry point for execution.
#############################################################################
# Copyright (C) 2012
#
# Authors: Dan Larkin <danielhlarkin@gmail.com>
#          Yonatan Naamad <cached@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#############################################################################

import struct
import socket
import logging
import threading, thread
import time
import signal
import sys

import dcontrol
from params import Params
from clientlistener import ClientListener
import network
import torrent
from torrent import Torrent

__author__ = "Daniel Larkin and Yonatan Naamad"
__copyright__ = "Copyright 2011, Daniel Larkin and Yonatan Naamad"
__credits__ = ["Daniel Larkin", "Yonatan Naamad"]
__license__ = "GPL"
__version__ = "0.1.0a"
__maintainer__ = "Daniel Larkin and Yonatan Naamad"
__email__ = "danielhlarkin@gmail.com,cached@gmail.com"
__status__ = "Prototype"


HISTORY_DEPTH = 5
CHECK_PERIOD = 5

DOWNWARD_BOUND = .7
DOWNWARD_SCALE = .8

UPWARD_BOUND = .9
UPWARD_REQUEST = .5

REQUEST_DELAY_TIME = 60
FIRST_FREE_WAIT_TIME = 60

class Client(object):
    def __init__(self, params):
        signal.signal(signal.SIGINT, self.signal_handler)
        logging.info("Initializing signal handler.")
        self.p = params
        self.active_torrents = set()
        self.delete_queue = []

        #initialize deluge connection
        thread.start_new_thread(dcontrol.init, (self.p['TORRENT_FILES_DIR'], self.p['HOSTED_FILES_DIR']))
        time.sleep(2)

        #initialize the periodic checker
        self.start_periodic_checker(CHECK_PERIOD)
        
        #pass some parameters to the Torrent class
        Torrent.data_dir = self.p['CPM_DATA_FILES_DIR']
        Torrent.database_ip = self.p['TORRENT_SERVER_IP']
        Torrent.database_port = self.p['TORRENT_SERVER_PORT']
        Torrent.dl_timeout = self.p['TORRENT_DL_TIMEOUT']
        Torrent.history_depth = HISTORY_DEPTH
        Torrent.client = self

        self.storage_pool = p['MAX_DISK_USAGE'] * 1024 * 1024
        self.bandwidth_pool = p['MAX_BANDWIDTH']

        #load torrents from save file
        logging.info("Loading torrents from save file.")
        torrent.load_torrents()
        
        logging.info("Resuming active torrents: %s." % self.active_torrents)
        self.deletable_files = set()
        self.connect_to_server()
        
    def connect_to_server(self):
        '''Establish connection to the server, execute initial handshake,
           and initialize listener for incoming requests.'''
        
        #Establish connection
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.settimeout(35.000)
        logging.info("Attempting to connect to %s:%d." % (self.p['SERVER_IP'],self.p['SERVER_PORT']))
        self.socket.connect((self.p['SERVER_IP'],self.p['SERVER_PORT']))
        logging.info("Connected to server.")
        
        #Begin the listener for handling server requests
        self.listener = ClientListener(self)
        self.listener.start()

        #set up join dictionary
        extra = []
        for t in self.active_torrents:
            extra += [struct.pack("!20s I I I", t.get_bin(), t.get_payload_size(), t.min_alloc, t.max_alloc)]

        d = {'k': self.p['SERVER_PASSKEY'], 'b_total': self.p['MAX_BANDWIDTH'],
             'd': self.p['MAX_DISK_USAGE'], 'n_max': self.p['TORRENT_LIMIT'],
             'n_current': len(self.active_torrents), 'L': extra}
        
        #Send initial handshake
        network.send_packet(self.socket, network.JOIN_op, network.JOIN_schema, d)
        
    def allocate_space(self, torrent):
        '''Gets space for the torrent. If not enough space is available,
           it deletes from the deletion pool until enough space is.'''
        space = torrent.get_payload_size()
        while True:
            if self.storage_pool > space:
                self.storage_pool -= space
                return True
            else:
                rm = self.delete_queue.pop(0)
                rm.delete_files()
                self.storage_pool += rm.payload_size
            if self.delete_queue == []: break
            
        logging.error("Not enough space (%d requested, %d free)." % 
                      (space, self.storage_pool))
        return False
            
    def free_space(self, torrent):
        '''Adds the space allocated for the torrent to the deletion pool.'''
        space = torrent.get_payload_size()
        if torrent in self._space_users:
            self.delete_queue.append(torrent)
            
        self.storage_pool += space
        
    def __del__(self):
        dcontrol.disconnect()
        self.socket.close()
        self.listener.stop()
        
    def signal_handler(self, signal, frame):
        logging.error("Received SIGINT. Quitting.")
        sys.exit(0)
        
    def start_periodic_checker(self, period):
        '''Creates a thread for the periodic checker.'''
        return thread.start_new_thread(self._periodic_update, (period,))
        
    def _periodic_update(self, period):
        '''Every 'period' seconds, check to see if some torrent has been
           either over or under active recently. If so, send corresponding
           FREE or REQUEST request and act accordingly.'''
           
        while True:
            import time
            time.sleep(period)
            #for each torrent, compare its current utilization to its caps
            for i in set(self.active_torrents):
                up_rate = dcontrol.get_upload_rate(i)
                i.recent_up_rates = i.recent_up_rates[1:] + [up_rate]
                logging.info("Recent up rate for %s is %s" % (i, i.recent_up_rates))
                if None in i.recent_up_rates: 
                    continue
                    
                if (max(i.recent_up_rates) < i.max_alloc * DOWNWARD_BOUND and i.jointime != None
                    and time.time() - i.jointime > FIRST_FREE_WAIT_TIME):
                    new_up = max(i.min_alloc, int(i.max_alloc * DOWNWARD_SCALE))
                    old_up = i.max_alloc
                    if old_up > new_up:
                        self.free(i, old_up - new_up)
                        
                elif (min(i.recent_up_rates) > i.max_alloc * UPWARD_BOUND and 
                      i.next_request < time.time()):
                    self.request(i, i.max_alloc * UPWARD_REQUEST)
                    i.next_request = (time.time() + REQUEST_DELAY_TIME)
            
    def free(self, torrent, amount):
        '''Reduce the torrent utilization by 'amount' and send out
           a FREE request to the server. '''
        torrent.set_allocations(max_alloc = torrent.max_alloc - amount)
        d = {'b_free': amount, 'T': torrent.get_bin()}
        network.send_packet(self.socket, network.FREE_op, network.FREE_schema, d)
        torrent.recent_up_rates[-1] = None

    def request(self, torrent, amount):
        '''Tell the server that the torrent needs 'amount' extra
           units of bandwidth.'''
        d = {'b': amount, 'T': torrent.get_bin()}
        network.send_packet(self.socket, network.REQUEST_op, network.REQUEST_schema, d)

if __name__ == '__main__':
    p = Params("config/params.conf", "config/schema")
    logging.basicConfig(level = getattr(logging, p['LOG_LEVEL'].upper()), format='%(levelname)s:%(message)s')
    c = Client(p)
